package com.mail.pop3;

import java.io.BufferedInputStream;
import java.io.BufferedWriter;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.StringTokenizer;

import com.mail.io.LineInputStream;
import com.mail.io.SharedByteArrayOutputStream;

/**
 * This class provides a POP3 connection and implements the POP3 protocol
 * requests.
 * 
 * @author Bill Shannon
 */
public class Pop3Protocol {
	private Socket socket; // POP3 socket
	private DataInputStream input; // input buf
	private PrintWriter output; // output buf
	private static final int POP3_PORT = 110; // standard POP3 port
	private static final String CRLF = "\r\n";
	private boolean debug = false;
	private PrintStream out;

	/**
	 * Open a connection to the POP3 server.
	 */
	@SuppressWarnings("finally")
	public Pop3Protocol(String host, int port, boolean debug, PrintStream out)
			throws IOException {
		this.debug = debug;
		this.out = out;
		Response r;
		try {
			if (port == -1)
				port = POP3_PORT;
			if (debug)
				out.println("DEBUG POP3: connecting to host \"" + host
						+ "\", port " + port);

			socket = SocketFetcher.getSocket(host, port);

			input = new DataInputStream(new BufferedInputStream(
					socket.getInputStream()));
			output = new PrintWriter(new BufferedWriter(new OutputStreamWriter(
					socket.getOutputStream(), "iso-8859-1")));
			// should be US-ASCII, but not all JDK's support

			r = simpleCommand(null);
		} catch (IOException ioe) {
			try {
				socket.close();
			} finally {
				throw ioe;
			}
		}

		if (!r.ok) {
			try {
				socket.close();
			} finally {
				throw new IOException("Connect failed");
			}
		}
	}

	protected void finalize() throws Throwable {
		super.finalize();
		if (socket != null) { // Forgot to logout ?!
			quit();
		}
	}

	/**
	 * Login to the server, using the USER and PASS commands.
	 */
	public synchronized String login(String user, String password)
			throws IOException {
		Response r;
		r = simpleCommand("USER " + user);
		if (!r.ok)
			return r.data != null ? r.data : "USER command failed";
		r = simpleCommand("PASS " + password);
		if (!r.ok)
			return r.data != null ? r.data : "login failed";
		return null;
	}

	/**
	 * Close down the connection, sending the QUIT command if expunge is true.
	 */
	synchronized boolean quit() throws IOException {
		boolean ok = false;
		try {
			Response r = simpleCommand("QUIT");
			ok = r.ok;
		} finally {
			try {
				socket.close();
			} finally {
				socket = null;
				input = null;
				output = null;
			}
		}
		return ok;
	}

	/**
	 * Return the total number of messages and mailbox size, using the STAT
	 * command.
	 */
	public synchronized Status stat() throws IOException {
		Response r = simpleCommand("STAT");
		Status s = new Status();
		if (r.ok && r.data != null) {
			try {
				StringTokenizer st = new StringTokenizer(r.data);
				s.setTotal(Integer.parseInt(st.nextToken()));
				s.setSize(Integer.parseInt(st.nextToken()));
			} catch (Exception e) {
			}
		}
		return s;
	}

	/**
	 * Return the size of the message using the LIST command.
	 */
	public synchronized int list(int msg) throws IOException {
		Response r = simpleCommand("LIST " + msg);
		int size = -1;
		if (r.ok && r.data != null) {
			try {
				StringTokenizer st = new StringTokenizer(r.data);
				st.nextToken(); // skip message number
				size = Integer.parseInt(st.nextToken());
			} catch (Exception e) {
			}
		}
		return size;
	}

	/**
	 * Retrieve the specified message. Given an estimate of the message's size
	 * we can be more efficient, preallocating the array and returning a
	 * SharedInputStream to allow us to share the array.
	 */
	public synchronized InputStream retr(int msg, int size) throws IOException {
		Response r = multilineCommand("RETR " + msg, size);
		return r.bytes;
	}

	/**
	 * Return the message header and the first n lines of the message.
	 */
	public synchronized InputStream top(int msg, int n) throws IOException {
		Response r = multilineCommand("TOP " + msg + " " + n, 0);
		return r.bytes;
	}

	/**
	 * Delete (permanently) the specified message.
	 */
	public synchronized boolean dele(int msg) throws IOException {
		Response r = simpleCommand("DELE " + msg);
		return r.ok;
	}

	/**
	 * Return the UIDL string for the message.
	 */
	synchronized String uidl(int msg) throws IOException {
		Response r = simpleCommand("UIDL " + msg);
		if (!r.ok)
			return null;
		int i = r.data.indexOf(' ');
		if (i > 0)
			return r.data.substring(i + 1);
		else
			return null;
	}

	/**
	 * Return the UIDL strings for all messages. The UID for msg #N is returned
	 * in uids[N-1].
	 */
	synchronized boolean uidl(String[] uids) throws IOException {
		Response r = multilineCommand("UIDL", 15 * uids.length);
		if (!r.ok)
			return false;
		LineInputStream lis = new LineInputStream(r.bytes);
		String line = null;
		while ((line = lis.readLine()) != null) {
			int i = line.indexOf(' ');
			if (i < 1 || i >= line.length())
				continue;
			int n = Integer.parseInt(line.substring(0, i));
			if (n > 0 && n <= uids.length)
				uids[n - 1] = line.substring(i + 1);
		}
		return true;
	}

	/**
	 * Do a NOOP.
	 */
	synchronized boolean noop() throws IOException {
		Response r = simpleCommand("NOOP");
		return r.ok;
	}

	/**
	 * Do an RSET.
	 */
	synchronized boolean rset() throws IOException {
		Response r = simpleCommand("RSET");
		return r.ok;
	}

	/**
	 * Issue a simple POP3 command and return the response.
	 */
	private Response simpleCommand(String cmd) throws IOException {
		if (socket == null)
			throw new IOException("Socket is closed");
		if (cmd != null) {
			if (debug)
				out.println("C: " + cmd);
			cmd += CRLF;
			output.print(cmd); // do it in one write
			output.flush();
		}
		@SuppressWarnings("deprecation")
		String line = input.readLine();
		if (line == null) {
			if (debug)
				out.println("S: EOF");
			throw new EOFException("EOF on socket");
		}
		if (debug)
			out.println("S: " + line);
		Response r = new Response();
		if (line.startsWith("+OK"))
			r.ok = true;
		else if (line.startsWith("-ERR"))
			r.ok = false;
		else
			throw new IOException("Unexpected response: " + line);
		int i;
		if ((i = line.indexOf(' ')) >= 0)
			r.data = line.substring(i + 1);
		return r;
	}

	/**
	 * Issue a POP3 command that expects a multi-line response.
	 * <code>size</code> is an estimate of the response size.
	 */
	private Response multilineCommand(String cmd, int size) throws IOException {
		Response r = simpleCommand(cmd);
		if (!r.ok)
			return (r);

		SharedByteArrayOutputStream buf = new SharedByteArrayOutputStream(size);
		int b, lastb = '\n';
		while ((b = input.read()) >= 0) {
			if (lastb == '\n' && b == '.') {
				b = input.read();
				if (b == '\r') {
					// end of response, consume LF as well
					input.read();
					break;
				}
			}
			buf.write(b);
			if (debug)
				out.write(b);
			lastb = b;
		}
		if (b < 0)
			throw new EOFException("EOF on socket");
		r.bytes = buf.toStream();
		return r;
	}
}
